1 /**
2  * Rotation manipulation feature
3  * 
4  * License:
5  *              Copyright Devisualization (Richard Andrew Cattermole) 2014 - 2017.
6  *     Distributed under the Boost Software License, Version 1.0.
7  *        (See accompanying file LICENSE_1_0.txt or copy at
8  *              http://www.boost.org/LICENSE_1_0.txt)
9  */
10 module devisualization.image.manipulation.rotation;
11 import devisualization.image.interfaces;
12 import devisualization.image.primitives : isImage, ImageColor, isPixelRange, PixelRangeColor;
13 import std.experimental.color : isColor;
14 import stdx.allocator : make, theAllocator, IAllocator;
15 
16 ///
17 enum RotateDirection : bool {
18     ///
19     ClockWise,
20 
21     ///
22     AntiClockWise
23 }
24 
25 /*
26  * Set rotation 90
27  */
28 
29 /**
30  * Rotates an image 90 degrees in place.
31  * 
32  * Params:
33  *      from        =   The image to rotate
34  *      direction   =   The direction to rotate
35  * 
36  * Returns:
37  *      The input image for chainability reasons.
38  */
39 Image rotate90(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) {
40     // 0 => 1
41     // 1 => 2
42     // 2 => 3
43     // 3 => 0
44     
45     return perform4WaySwap(from, direction, 1);
46 }
47 
48 /**
49  * Rotates an image 90 degrees
50  * 
51  * Wraps the input range varient of this to take an image instead.
52  * 
53  * Params:
54  *      image       =   The image to rotate
55  *      direction   =   The direction to rotate
56  *      allocator   =   The allocator to allocate an input range from
57  * 
58  * Returns:
59  *      An input range that has an ElementType of PixelPoint.
60  */
61 auto rotate90Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) {
62     return rotate90Range(from.rangeOf(allocator), direction);
63 }
64 
65 /**
66  * Rotates an image 90 degrees
67  * 
68  * Params:
69  *      from        =   Input range to take pixels from
70  *      direction   =   The direction to rotate
71  *      allocator   =   The allocator to allocate an input range from
72  * 
73  * Returns:
74  *      An input range that has an ElementType of PixelPoint.
75  */
76 auto rotate90Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) {
77     return RotateN!(PixelRangeColor!IR, IR)(from, 1, direction);
78 }
79 
80 /*
81  * Set rotation 180
82  */
83 
84 /**
85  * Rotates an image 180 degrees in place.
86  * 
87  * Params:
88  *      from        =   The image to rotate
89  *      direction   =   The direction to rotate
90  * 
91  * Returns:
92  *      The input image for chainability reasons.
93  */
94 Image rotate180(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) {
95     // 0 => 2
96     // 1 => 3
97     // 2 => 0
98     // 3 => 1
99 
100     return perform4WaySwap(from, direction, 2);
101 }
102 
103 /**
104  * Rotates an image 180 degrees
105  * 
106  * Wraps the input range varient of this to take an image instead.
107  * 
108  * Params:
109  *      image       =   The image to rotate
110  *      direction   =   The direction to rotate
111  *      allocator   =   The allocator to allocate an input range from
112  * 
113  * Returns:
114  *      An input range that has an ElementType of PixelPoint.
115  */
116 auto rotate180Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) {
117     return rotate180Range(from.rangeOf(allocator), direction);
118 }
119 
120 /**
121  * Rotates an image 180 degrees
122  * 
123  * Params:
124  *      from        =   Input range to take pixels from
125  *      direction   =   The direction to rotate
126  *      allocator   =   The allocator to allocate an input range from
127  * 
128  * Returns:
129  *      An input range that has an ElementType of PixelPoint.
130  */
131 auto rotate180Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) {
132     return RotateN!(PixelRangeColor!IR, IR)(from, 2, direction);
133 }
134 
135 /*
136  * Set rotation 270
137  */
138 
139 /**
140  * Rotates an image 270 degrees in place.
141  * 
142  * Params:
143  *      from        =   The image to rotate
144  *      direction   =   The direction to rotate
145  * 
146  * Returns:
147  *      The input image for chainability reasons.
148  */
149 Image rotate270(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise) if (isImage!Image) {
150     // 0 => 3
151     // 1 => 0
152     // 2 => 1
153     // 3 => 2
154     
155     return perform4WaySwap(from, direction, 3);
156 }
157 
158 /**
159  * Rotates an image 270 degrees
160  * 
161  * Wraps the input range varient of this to take an image instead.
162  * 
163  * Params:
164  *      image       =   The image to rotate
165  *      direction   =   The direction to rotate
166  *      allocator   =   The allocator to allocate an input range from
167  * 
168  * Returns:
169  *      An input range that has an ElementType of PixelPoint.
170  */
171 auto rotate270Range(Image)(Image from, RotateDirection direction = RotateDirection.ClockWise, IAllocator allocator = theAllocator()) if (isImage!Image) {
172     return rotate270Range(from.rangeOf(allocator), direction);
173 }
174 
175 /**
176  * Rotates an image 270 degrees
177  * 
178  * Params:
179  *      from        =   Input range to take pixels from
180  *      direction   =   The direction to rotate
181  *      allocator   =   The allocator to allocate an input range from
182  * 
183  * Returns:
184  *      An input range that has an ElementType of PixelPoint.
185  */
186 auto rotate270Range(IR)(IR from, RotateDirection direction = RotateDirection.ClockWise) if (isPixelRange!IR) {
187     return RotateN!(PixelRangeColor!IR, IR)(from, 3, direction);
188 }
189 
190 /*
191  * Abituary rotation
192  */
193 
194 /**
195  * Simple shear+clip rotation.
196  * 
197  * Uses radians.
198  */
199 Image rotate(Impl, Image)(Image from, double by, IAllocator allocator = theAllocator()) if (isImage!Impl && isImage!Image)
200 in {
201     assert(by <= D_PI);
202 } body {
203     import std.math : cos, sin;
204     import std.typecons : tuple;
205 
206 	if (by > 0) {
207 		while(by > D_PI) {
208 			by -= D_PI;
209 		}
210 	} else {
211 		while (by < -D_PI) {
212 			by += D_PI;
213 		}
214 	}
215 
216     auto newSize = calculateNewSize(from.width, from.height, by);
217     Impl ret = allocator.make!Impl(newSize[0], newSize[1], allocator);
218 
219     foreach(toX; 0 .. newSize[0]) {
220         foreach(toY; 0 .. newSize[1]) {
221             auto destCoord = oldCoord(toX, toY, by);
222 
223 			ret[destCoord.x, destCoord.y] = from[destCoord.x, destCoord.y];
224         }
225     }
226 
227     return ret;
228 }
229 
230 private {
231     import std.math : PI_2, PI, PI_4;
232     import std.typecons : Tuple, tuple;
233 
234     enum PI_34 = 3 * PI_4;
235     enum D_PI = 2 * PI;
236 
237     // TODO: unittests!
238     Tuple!(size_t, size_t) calculateNewSize(size_t oldWidth, size_t oldHeight, double by) {
239         import std.math : cos, sin;
240         
241         double newWidth, newHeight;
242         
243         //L * cos(t) + H
244         newWidth = oldWidth * cos(by) + oldHeight;
245         //L * sin(t) + H
246         newHeight = oldWidth * sin(by) + oldHeight;
247         
248         double by2;
249         if (by <= PI_2) //(90 – t)
250             by2 = PI_2 - by;
251         else if (by <= PI) //(180 – t)
252             by2 = PI - by;
253         else if (by <= PI_34) //(t – 180)
254             by2 = by - PI_34;
255         else if (by <= D_PI) //(360 – t)
256             by2 = D_PI - by;
257         
258         // * cos(90 – t)
259         newWidth *= cos(by2);
260         // * sin(90 – t)
261         newHeight *= sin(by2);
262 
263         return tuple(cast(size_t)newWidth, cast(size_t)newHeight);
264     }
265 
266     // TODO: unittests!
267     Tuple!(size_t, size_t) oldCoord(size_t toX, size_t toY, double by) {
268         import std.math : cos, sin;
269 
270         double a = cos(by);
271         double b = sin(by);
272         double determinant = a*a-b*-b;
273 
274         double x = (toX * a) + (toY * b);
275         double y = (toX * -b) + (toY * a);
276         x /= determinant;
277         y /= determinant;
278 
279         // FIXME
280         assert(x >= 0);
281         assert(y >= 0);
282         
283         return tuple(cast(size_t)x, cast(size_t)y);
284     }
285 
286     size_t[4][2] coords4WaySwap(size_t width, size_t height, size_t x, size_t y, RotateDirection direction) @safe
287     in {
288         import std.math : floor;
289         import std.exception : enforce;
290 
291         enforce(x <= floor(width / 2f));
292         enforce(y <= floor(height / 2f));
293     } body {
294         if (direction == RotateDirection.ClockWise) {
295             // 0 1 2 3
296             return [[
297                     x,
298                     y,
299                     width - (x+1),
300                     width - (y+1)
301                 ], [
302                     y,
303                     x,
304                     height - (y+1),
305                     height - (x+1)
306                 ]];
307         } else if (direction == RotateDirection.AntiClockWise) {
308             // 2 3 0 1
309             
310             return [[
311                     width - (x+1),
312                     width - (y+1),
313                     x,
314                     y
315                 ], [
316                     height - (y+1),
317                     height - (x+1),
318                     y,
319                     x
320                 ]];
321         }
322 
323         assert(0);
324     }
325 
326     unittest {
327         import std.exception : assertThrown, assertNotThrown;
328 
329         // contract checks
330 
331         assertThrown(coords4WaySwap(3, 3, 2, 1, RotateDirection.ClockWise));
332         assertNotThrown(coords4WaySwap(3, 3, 1, 0, RotateDirection.ClockWise));
333         assertThrown(coords4WaySwap(3, 3, 2, 1, RotateDirection.ClockWise));
334     }
335 
336     unittest {
337         size_t[4][2] v;
338 
339         // if condition check
340 
341         v = coords4WaySwap(3, 3, 0, 1, RotateDirection.ClockWise);
342         assert(v[0][0] == 0);
343         assert(v[0][1] == 1);
344         assert(v[1][0] == 1);
345         assert(v[1][1] == 0);
346 
347         assert(v[0][2] == 2);
348         assert(v[0][3] == 1);
349         assert(v[1][2] == 1);
350         assert(v[1][3] == 2);
351     }
352 
353     unittest {
354         size_t[4][2] v;
355 
356         // clockwise
357 
358         v = coords4WaySwap(3, 3, 0, 1, RotateDirection.AntiClockWise);
359         assert(v[0][2] == 0);
360         assert(v[0][3] == 1);
361         assert(v[1][2] == 1);
362         assert(v[1][3] == 0);
363         
364         assert(v[0][0] == 2);
365         assert(v[0][1] == 1);
366         assert(v[1][0] == 1);
367         assert(v[1][1] == 2);
368     }
369 
370     // TODO: unittests!
371     Image perform4WaySwap(Image)(Image from, RotateDirection direction, ubyte idxAdd) if (isImage!Image) {
372         foreach(x; 0 .. (from.width / 2)) {
373             foreach(y; 0 .. cast(size_t)(from.height / 2)) {
374                 size_t[4][2] coords = coords4WaySwap(from.width, from.height, x, y, direction);
375                 
376                 Color[4] temp = [
377                     from[coords[0][0], coords[1][0]], from[coords[0][1], coords[1][1]],
378                     from[coords[0][2], coords[1][2]], from[coords[0][3], coords[1][3]]
379                 ];
380                 
381                 ubyte nidx;
382 
383                 nidx = (0 + idxAdd) % 4;
384                 from[coords[0][nidx], coords[1][nidx]] = temp[0];
385                 nidx = (1 + idxAdd) % 4;
386                 from[coords[0][nidx], coords[1][nidx]] = temp[1];
387                 nidx = (2 + idxAdd) % 4;
388                 from[coords[0][nidx], coords[1][nidx]] = temp[2];
389                 nidx = (3 + idxAdd) % 4;
390                 from[coords[0][nidx], coords[1][nidx]] = temp[3];
391             }
392         }
393         
394         return from;
395     }
396 
397     // TODO: unittests
398     struct RotateN(Color, IRRange) {
399         IRRange input;
400         PixelPoint!Color current;
401         ubyte idxToAdd;
402         RotateDirection direction;
403 
404         this(IRRange input, ubyte idxToAdd, RotateDirection direction) {
405             this.input = input;
406             this.idxToAdd = idxToAdd;
407             this.direction = direction;
408             popFront;
409         }
410 
411         @property {
412             PixelPoint!Color front() {
413                 return current;
414             }
415 
416             bool empty() {
417                 return input.empty;
418             }
419         }
420 
421         void popFront() {
422             if (!empty) {
423                 PixelPoint got = input.front;
424 
425                 size_t[4][2] coords = coords4WaySwap(got.width, got.height, got.x, got.y, direction);
426                 ubyte corner;
427 
428                 if (got.x < got.width / 2) {
429                     if (got.y < got.height / 2) {
430                         corner = 0;
431                     } else {
432                         corner = 3;
433                     }
434                 } else {
435                     if (got.y < got.height / 2) {
436                         corner = 1;
437                     } else {
438                         corner = 2;
439                     }
440                 }
441 
442                 ubyte nidx;
443                 nidx = (corner + idxAdd) % 4;
444                 current = PixelPoint!Color(got.value, coords[0][nidx], coords[1][nidx], got.width, got.height);
445 
446                 input.popFront;
447             }
448         }
449     }
450 }